[Previous] [Next]

ListBox and ComboBox Controls

ListBox and ComboBox controls share many properties, methods, and events. ListBox controls are somewhat more powerful, so let's start with them. Explaining ComboBox controls afterward will be a walk in the park.

ListBox Controls

Once you have dropped a ListBox control on a form's surface, you might need to assign a few properties. For example, you set the Sorted attribute to True to create ListBox controls that automatically sort their items in alphabetical order. By acting on the Columns property, you create a different type of list box, with several columns and a horizontal scroll bar, as you can see in Figure 3-6, instead of the default list box with a single column and a vertical scroll bar along its right border. You can make assignments for both these properties only at design time, and you can't change the style of the ListBox control while the program is running.

Click to view at full size.

Figure 3-6. Effects of different settings for the Column property.

The IntegralHeight property is seldom modified, but it deserves some explanation because it indirectly gets in the way during regular programming. By default, Visual Basic automatically adjusts the height of ListBox controls so that they display entire rows; no item is shown only partially. The exact height assigned to the control depends on several factors, including current font attributes. This behavior is usually OK, and you normally don't have to worry about this issue. But if you want to resize the control to align it with other controls on the form or with the form's border, this feature might prevent you from doing such an adjustment. In this case, you should set the IntegralHeight property to False in the Properties window: Visual Basic won't enforce a particular height and you're free to resize the control as you prefer. Unfortunately, you can't modify this property at run time.

If you know at design time which items must appear in the ListBox control, you can save some code and enter the items right in the Properties window, in the List property mini-editor, as you can see in Figure 3-7. But if you're going to enter more than four or five items, you'd probably better add them via code at run time.

Figure 3-7. Entering items at design time.(Press Ctrl+Enter to move to the next line.)

Both ListBox and ComboBox controls expose the AddItem method, which lets you add items when the program is executing. You usually use this method in the Form_Load event procedure:

Private Sub Form_Load()
    List1.AddItem "First"
    List1.AddItem "Second"
    List1.AddItem "Third"
End Sub

In real-world applications, you rarely load individual items in this way. Most often your data is already stored in an array or in a database, and you have to scan the source of your data with a For…Next loop, as in the following code:

' MyData is an array of strings.
For i = LBound(MyData) To UBound(MyData)
    List1.AddItem MyData(i)
Next

TIP
If you want to load many items in a list box but don't want to create an array, you can resort to Visual Basic's Choose function, as follows:

For i = 1 To 5
    List1.AddItem Choose(i, "America", "Europe", "Asia", _

        "Africa", "Australia")"

Next

Some special cases don't even require you to list individual items:

' The names of the months (locale-aware)
For i = 1 To 12

    List1.AddItem MonthName(i)
Next

' The names of the days of the week (locale-aware)
For i = 1 To 7
    List1.AddItem WeekDayName(i)
Next

MonthName and WeekDayName are new Visual Basic string functions, described in Chapter 5.

If you want to load dozens or hundreds of items, a better approach is to store them in a text file and have your program read the file when the form loads. This way you can later change the contents of your ListBox controls without recompiling the source code:

Private Sub Form_Load()
    Dim item As String 
    On Error Goto Error_Handler
    Open "listbox.dat" For Input As #1
    Do Until EOF(1)
        Line Input #1, item
        List1.AddItem item
    Loop
    Close #1
    Exit Sub
Error_Handler:
    MsgBox "Unable to load Listbox data"
End Sub       

Sometimes you need to add an item in a given position, which you do by passing a second argument to the AddItem method. (Note that indexes are zero-based.)

' Add at the very beginning of the list.
List1.AddItem "Zero", 0

This argument has precedence over the Sorted attribute, so you can actually insert some items out of order even in sorted ListBox controls. Removing items is easy with the RemoveItem or Clear methods:

' Remove the first item in the list.
List1.RemoveItem 0
' Quickly remove all items (no need for a For...Next loop).
List1.Clear

The most obvious operation to be performed at run time on a filled ListBox control is to determine which item has been selected by the user. The ListIndex property returns the index of the selected item (zero-based), while the Text property returns the actual string stored in the ListBox. The ListIndex property returns -1 if the user hasn't selected any element yet, so you should test for this condition first:

If List1.ListIndex = -1 Then
    MsgBox "No items selected"
Else
    MsgBox "User selected " & List1.Text & " (#" & List1.ListIndex & ")"
End If

You can also assign a value to the ListIndex property to programmatically select an item, or set it to -1 to deselect all items:

' Select the third item in the list.
List1.ListIndex = 2

The ListCount property returns the number of items in the control. You can use it with the List property to enumerate them:

For i = 0 To List1.ListCount -1
    Print "Item #" & i & " = " & List1.List(i)
Next

Reacting to user actions

If your form doesn't immediately react to a user's selections on the ListBox control, you don't have to write any code to handle its events. But this is true only for trivial Visual Basic applications. In most cases, you'll have to respond to the Click event, which occurs whenever a new element has been selected (with the mouse, with the keyboard, or programmatically):

Private Sub List1_Click()
    Debug.Print "User selected item #" & List1.ListIndex
Next

The logic behind your user interface might require that you monitor the DblClick event as well. As a general rule, double-clicking on a ListBox control's item should have the same effect as selecting the item and then clicking on a push button (often the default push button on the form). Take, for example, the mutually exclusive ListBox controls shown in Figure 3-8, a type of user interface that you see in many Windows applications. Implementing this structure in Visual Basic is straightforward:

Click to view at full size.

Figure 3-8. A pair of mutually exclusive ListBox controls. You can move items around using the buttons in the middle or by double-clicking them.

Private Sub cmdMove_Click()
    ' Move one item from left to right.
    If lstLeft.ListIndex >= 0 Then
        lstRight.AddItem lstLeft.Text
        lstLeft.RemoveItem lstLeft.ListIndex
    End If
End Sub

Private Sub cmdMoveAll_Click()
    ' Move all items from left to right.
    Do While lstLeft.ListCount
        lstRight.AddItem lstLeft.List(0)
        lstLeft.RemoveItem 0
    Loop
End Sub

Private Sub cmdBack_Click()
    ' Move one item from right to left.
    If lstRight.ListIndex >= 0 Then
        lstLeft.AddItem lstRight.Text
        lstRight.RemoveItem lstRight.ListIndex
    End If
End Sub

Private Sub cmdBackAll_Click()
    ' Move all items from right to left.
    Do While lstRight.ListCount
        lstLeft.AddItem lstRight.List(0)
        lstRight.RemoveItem 0
    Loop
End Sub

Private Sub lstLeft_DblClick()
    ' Simulate a click on the Move button.
    cmdMove.Value = True
End Sub

Private Sub lstRight_DblClick()
    ' Simulate a click on the Back button.
    cmdBack.Value = True
End Sub

The Scroll event comes in handy when you need to synchronize a ListBox control with another control, often another list box; in such cases, you usually want to scroll the two controls together, so you need to know when either one is scrolled. The Scroll event is often used in conjunction with the TopIndex property, which sets or returns the index of the first visible item in the list area. Using the Scroll event together with the TopIndex property, you can achieve really interesting visual effects, such as the one displayed in Figure 3-9. The trick is that the leftmost ListBox control is partially covered by the other control. Its companion scroll bar is never seen by users, who are led to believe that they're acting on a single control. For the best effect, you need to write code that keeps the two controls always in sync, and you achieve that by trapping Click, MouseDown, MouseMove, and Scroll events. The following code synchronizes two lists, lstN and lstSquare:

Figure 3-9. You don't need a grid control to simulate a simple table; two partially overlapping ListBox controls will suffice.

Private Sub lstN_Click()
    ' Synchronize list boxes.
    lstSquare.TopIndex = lstN.TopIndex
    lstSquare.ListIndex = lstN.ListIndex
End Sub
Private Sub lstSquare_Click()
    ' Synchronize list boxes.
    lstN.TopIndex = lstSquare.TopIndex
    lstN.ListIndex = lstSquare.ListIndex
End Sub

Private Sub lstN_MouseDown(Button As Integer, Shift As Integer, _
    X As Single, Y As Single)
    Call lstN_Click
End Sub
Private Sub lstSquare_MouseDown(Button As Integer, _
    Shift As Integer, X As Single, Y As Single)
    Call lstSquare_Click
End Sub

Private Sub lstN_MouseMove(Button As Integer, Shift As Integer, _
    X As Single, Y As Single)
    Call lstN_Click
End Sub
Private Sub lstSquare_MouseMove(Button As Integer, _
    Shift As Integer, X As Single, Y As Single)
    Call lstSquare_Click
End Sub

Private Sub lstN_Scroll()
    lstSquare.TopIndex = lstN.TopIndex
End Sub
Private Sub lstSquare_Scroll()
    lstN.TopIndex = lstSquare.TopIndex
End Sub

The ItemData property

The information you place in a ListBox control is rarely independent from the rest of the application. For example, the customer's name that you see on screen is often related to a corresponding CustomerID number, a product name is associated with its description, and so on. The problem is that once you load a value into the ListBox control you somehow disrupt such relationships; the code in event procedures sees only ListIndex and List properties. How can you retrieve the CustomerID value that was originally associated with the name that the user has just clicked on? The answer to this question is provided by the ItemData property, which lets you associate a 32bit integer value with each item loaded in the ListBox control, as in the code below.

' Add an item to the end of the list.
lstCust.AddItem CustomerName
' Remember the matching CustomerID.
lstCust.ItemData(lstCust.ListCount  -1) = CustomerId

Note that you must pass an index to the ItemData property: Because the item you have just added is now the last one in the ListBox control, its index is ListCount-1. Unfortunately, this simple approach doesn't work with sorted ListBox controls, which can place new items anywhere in the list. In this case, you use the NewIndex property to find out where an item has been inserted:

' Add an item to the end of the list.
lstCust.AddItem CustomerName
' Remember the matching ID. (This also works with Sorted list boxes.)
lstCust.ItemData(lstCust.NewIndex) = CustomerId

In real-world applications, associating a 32-bit integer value with an item in a ListBox control is often inadequate, and you usually need to store more complex information. In this case, you use the ItemData value as an index into another structure, for example, an array of strings or an array of records. Let's say you have a list of product names and descriptions:

Type ProductUDT
    Name As String
    Description As String
    Price As Currency
End Type
Dim Products() As ProductUDT, i As Long

Private Sub Form_Load()
    ' Load product list from database into Products.
    ' ... (code omitted)
    ' Load product names into a sorted ListBox.
    For i = LBound(Products) To UBound(Products)
        lstProducts.AddItem Products(i).Name
        ' Remember where this product comes from.
        lstProducts.ItemData(lstProducts.NewIndex) = i
    Next
End Sub

Private Sub lstProducts_Click()
    ' Show the description and price of the item
    ' currently selected, using two companion labels.
    i = lstProducts.ItemData(lstProducts.ListIndex)
    lblDescription.Caption = Products(i).Description
    lblPrice.Caption = Products(i).Price
End Sub

Multiple-selection ListBox controls

The ListBox control is even more flexible than I've shown so far because it lets users select multiple items at the same time. To enable this feature, you assign the MultiSelect property the values 1-Simple or 2-Extended. In the former case, you can select and deselect individual items only by using the Spacebar or the mouse. In extended selection, you can also use the Shift key to select ranges of items. Most popular Windows programs use extended selection exclusively, so you shouldn't use the value 1-Simple unless you have a good reason to do so. The MultiSelect property can't be changed when the program is running, so this is a design-time decision.

Working with a multiple selection ListBox control isn't different from interacting with a regular ListBox in the sense that you still use the ListIndex, ListCount, List, and ItemData properties. In this case, the most important piece of information is held in the SelCount and Selected properties. The SelCount property simply returns the number of items that are currently selected. You usually test it within a Click event:

Private Sub lstProducts_Click()
    ' The OK button should be enabled only if the
    ' user has selected at least one product.
    cmdOK.Enabled = (lstProducts.SelCount > 0)
End Sub

You retrieve the items that are currently selected using the Selected property. For example, this routine prints all selected items:

' Print a list of selected products.
Dim i As Long
For i = 0 To lstProducts.ListCount -1
    If lstProducts.Selected(i) Then Print lstProducts.List(i)
Next

The Select property can be written to, which is sometimes necessary to clear the current selection:

For i = 0 To lstProducts.ListCount -1
    lstProducts.Selected(i) = False
Next

Visual Basic 5 introduced a new variant of multiple selection ListBox controls, which let users select items by flagging a check box, as you see in Figure 3-10. To enable this capability, you set the ListBox control's Style property to 1-Checkbox at design time. (You can't change it at run time.) ListBox controls with check boxes are always multiselect, and the actual value of the MultiSelect property is ignored. These ListBox controls let the user select and deselect one item at a time, so it's often convenient to provide the user with two buttons—Select All and Clear All (and sometimes Invert Selection too).

Click to view at full size.

Figure 3-10. Two variants for multiple selection ListBox controls.

Apart from their appearance, there's nothing special about ListBox controls set as Style = 1-Checkbox, in that you can set and query the selected state of items through the Selected property. However, selecting and deselecting multiple items through code doesn't happen as quickly as you might believe. For example, this is the code for handling the Click event of the Select All button:

Private Sub cmdSelectAll_Click()
    Dim i As Long, saveIndex As Long, saveTop As Long
    ' Save current state.
    saveIndex = List2.ListIndex
    saveTop = List2.TopIndex
    ' Make the list box invisible to avoid flickering.
    List2.Visible = False
    ' Change the select state for all items.
    For i = 0 To List2.ListCount - 1
        List2.Selected(i) = True
    Next
    ' Restore original state, and make the list box visible again.
    List2.TopIndex = saveTop
    List2.ListIndex = saveIndex
    List2.Visible = True
End Sub

The code for the Clear All and Invert All buttons is similar, except for the statement inside the For…Next loop. This approach is necessary because writing to the Selected property also affects the ListIndex property and causes a lot of flickering. Saving the current state in two temporary variables solves the former problem, while making the control temporarily invisible solves the latter.

Interestingly, making the control invisible doesn't actually hide it, not immediately at least. If you operate on a control and want to avoid flickering or other disturbing visual effects, make it invisible, do your stuff, and then make it visible again before the procedure ends. If the procedure doesn't include any DoEvents or Refresh statement, the screen isn't updated and the user will never notice that the control has been made temporarily invisible. To see how the code would work without resorting to this technique, add a DoEvents or a Refresh statement to the preceding code, immediately before the For…Next loop.

ListBox controls with Style = 1-Checkbox offer an additional event, ItemCheck, that fires when the user selects or deselects the check box. You can use this event to refuse to select or deselect a given item:

Private Sub List2_ItemCheck(Item As Integer)
    ' Refuse to deselect the first item.
    If Item = 0 And List2.Selected(0) = False Then
        List2.Selected(0) = True
        MsgBox "You can't deselect the first item", vbExclamation
    End If
End Sub

ComboBox Controls

ComboBox controls are very similar to ListBox controls, so much of what I have explained so far applies to them as well. More precisely, you can create ComboBox controls that automatically sort their items using the Sorted property, you can add items at design time using the List item in the Properties window, and you can set a ComboBox control's IntegralHeight property as your user interface dictates. Most run-time methods are common to both kinds of controls too, including AddItem, RemoveItem, and Clear, as are the ListCount, ListIndex, List, ItemData, TopIndex, and NewIndex properties and the Click, DblClick, and Scroll events. ComboBox controls don't support multiple columns and multiple selections, so you don't have to deal with the Column, MultiSelect, Select, and SelCount properties and the ItemCheck event.

The ComboBox control is a sort of mixture between a ListBox and a TextBox control in that it also includes several properties and events that are more typical of the latter, such as the SelStart, SelLength, SelText, and Locked properties and the KeyDown, KeyPress, and KeyUp events. I've already explained many things that you can do with these properties and won't repeat myself here. Suffice it to say that you can apply to ComboBox controls most of the techniques that are valid for TextBox controls, including automatic formatting and deformatting of data in GotFocus and LostFocus event procedures and validation in Validate event procedures.

The most characteristic ComboBox control property is Style, which lets you pick one among the three styles available, as you can see in Figure 3-11. When you set Style = 0-DropDown Combo, what you get is the classic combo; you can enter a value in the edit area or select one from the drop-down list. The setting Style = 1-Simple Combo is similar, but the list area is always visible so that in this case you really have a compounded TextBox plus ListBox control. By default, Visual Basic creates a control that's only tall enough to show the edit area, and you must resize it to make the list portion visible. Finally, Style = 2-Dropdown List suppresses the edit area and gives you only a drop-down list to choose from.

Figure 3-11. Three different styles for ComboBox controls. The drop-down list variant doesn't allow direct editing of the contents.

When you have a ComboBox control with Style = 0-Dropdown Combo or 2-Dropdown List, you can learn when the user is opening the list portion by trapping the DropDown event. For example, you can fill the list area just one instant before the user sees it (a sort of just-in-time data loading):

Private Sub Combo1_DropDown()
    Dim i As Integer
    ' Do it only once.
    If Combo1.ListCount = 0 Then
        For i = 1 To 100
            Combo3.AddItem "Item #" & i
        Next
    End If
End Sub

The ComboBox control supports the Click and DblClick events, but they relate only to the list portion of the control. More precisely, you get a Click event when the user selects an item from the list, and you get a DblClick event only when an item in the list is double-clicked. The latter can occur only when Style = 1-Simple Combo, though, and you'll never get this event for other types of ComboBox controls.

NOTE
For reasons that, honestly, are beyond my imagination, MouseDown, MouseUp, and MouseMove events aren't supported by the ComboBox intrinsic controls. Don't ask me why. Ask Microsoft.

ComboBox controls with Style = 1-Simple Combo possess an intriguing feature, called extended matching. As you type a string, Visual Basic scrolls the list portion so that the first visible item in the list area matches the characters in the edit area.

Drop-down list controls pose special problems in programming. For example, they never raise Change and keyboard-related events. Moreover, you can't reference all the properties that are related to activity in the edit area, such as SelStart, SelLength, and SelText. (You get error 380—"Invalid property value.") The Text property can be read and can also be written to, provided that the value you assign is among the items in the list. (Visual Basic performs a case-insensitive search.) If you try to assign a string that isn't in the list, you get a run-time error (383—"Text property is read-only"), which isn't really appropriate because the Text property can sometimes be assigned).